""" ATZLinux Store UI - 普通用户权限的界面程序 """
import os
import platform
import re
import json
import logging
from logging.handlers import TimedRotatingFileHandler
import signal
import threading
import time
from urllib.request import urlretrieve
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk

# 导入客户端通信库
from atzstore_client import AptClient

# 配置
atzdir = os.getenv('HOME') + '/.atzlinux'
curdir = os.path.realpath(os.path.dirname(__file__))
allpackages_cache = f'{atzdir}/allpackages.json'
allpackages_bundled = f'{curdir}/allpackages.json'
pkg_indexes = dict()

pidfile = atzdir + '/store.pid'
if not os.path.exists(atzdir):
    os.mkdir(atzdir)

# 日志配置
fmt = logging.Formatter('%(asctime)s - %(levelname)s %(message)s')
console = logging.StreamHandler()
console.setFormatter(fmt)

logfile = '%s/atzstore-ui.log' % atzdir
filelog = TimedRotatingFileHandler(logfile,
    when='D', backupCount=10, encoding='utf-8')
filelog.setFormatter(fmt)

logging.getLogger('').addHandler(console)
logging.getLogger('').addHandler(filelog)
logging.getLogger('').setLevel(logging.DEBUG)

class StoreWin:
    """ UI处理器 - 运行在普通用户权限 """
    columns = [
        ('selected', '选中'),
        ('pkg', '包名'),
        ('name', '应用名'),
        ('status', '安装状态'),
        ('dlsize', '下载大小'),
        ('instsize', '安装大小'),
        ('summary', '备注'),
    ]
    arch = platform.machine()
    builder = Gtk.Builder()
    win = Gtk.Window()
    treeview = Gtk.TreeView()
    model = Gtk.ListStore(bool, str, str, str, str, str, str)
    extra_infos = {}
    section = '全部'
    sections = ['全部']

    def __init__(self):
        """ 初始化 """
        self.apt_client = AptClient()
        self.setup_apt_client()
        GLib.idle_add(self.on_btn_refresh_clicked)
    
    def setup_apt_client(self):
        """配置APT客户端"""
        # 连接到守护进程
        if not self.apt_client.connect():
            self.show_error_dialog("无法连接到ATZStore守护进程", 
                                 "请确保守护进程已启动:\nsudo systemctl start atzstore-daemon")
            return
            
        # 设置消息回调
        self.apt_client.set_callback('command_output', self.on_command_output)
        self.apt_client.set_callback('command_complete', self.on_command_complete)
        self.apt_client.set_callback('command_error', self.on_command_error)
        self.apt_client.set_callback('installed_packages', self.on_installed_packages_received)
        
        logging.info("APT客户端初始化完成")

    def on_command_output(self, message):
        """处理命令输出"""
        output = message.get('output', '')
        GLib.idle_add(self.set_statusbar_text, output)
        
    def on_command_complete(self, message):
        """命令执行完成"""
        command = message.get('command', '')
        return_code = message.get('return_code', 0)
        
        if return_code == 0:
            GLib.idle_add(self.set_statusbar_text, f'完成命令: {command}')
            GLib.idle_add(self.refresh_packages)
        else:
            GLib.idle_add(self.set_statusbar_text, f'命令执行失败: {command} (返回码: {return_code})')
            
    def on_command_error(self, message):
        """命令执行错误"""
        error = message.get('error', '')
        command = message.get('command', '')
        GLib.idle_add(self.set_statusbar_text, f'命令错误: {command} - {error}')
        
    def on_installed_packages_received(self, message):
        """接收到已安装软件包列表"""
        packages = message.get('packages', {})
        self.installed_packages = packages
        GLib.idle_add(self.load_data_as_model)
    
    def load_data_as_model(self):
        """加载数据到模型"""
        f = allpackages_cache if os.path.exists(allpackages_cache) else allpackages_bundled
        logging.info(f"Loading packages list from {f}")
        
        try:
            with open(f, 'r', encoding='utf-8') as file:
                allpackages = json.load(file)
        except Exception as e:
            logging.error(f"Failed to load packages: {e}")
            return
            
        # 使用客户端获取的已安装包信息
        versions = getattr(self, 'installed_packages', {})
        
        idx = 0
        self.model = Gtk.ListStore(bool, str, str, str, str, str, str)
        for data in allpackages:
            pkg = data['pkg']
            self.extra_infos[pkg] = data
            info = {}
            info['selected'] = False
            info['pkg'] = pkg
            info['name'] = data['name']
            info['status'] = versions.get(pkg, '未安装')
            info['dlsize'] = data['dlsize']
            info['instsize'] = data['instsize']
            info['section'] = data.get('section', '全部')
            info['summary'] = re.sub(re.compile('<.*?>'), '', data['summary'])
            
            row = []
            for col in StoreWin.columns:
                row.append(info[col[0]])
            if info['section'] == self.section or self.section == '全部':
                self.model.append(row)
            pkg_indexes[pkg] = idx
            idx += 1
            
        if hasattr(self, 'treeview'):
            self.treeview.set_model(self.model)
    
    def col2pos(self, col):
        for idx, column in enumerate(self.columns):
            if column[0] == col:
                return idx

    def get_selected_packages(self):
        return [r[1] for r in self.model if r[0]]

    def messagebox(self, title, content):
        msg = Gtk.MessageDialog(self.win, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.CLOSE, title)
        msg.format_secondary_text(content)
        msg.run()
        msg.destroy()
        
    def show_error_dialog(self, title, content):
        """显示错误对话框"""
        msg = Gtk.MessageDialog(self.win, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, title)
        msg.format_secondary_text(content)
        msg.run()
        msg.destroy()

    def confirm_action(self, title, content, cb=None):
        confirm = Gtk.MessageDialog(self.win, 0, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK_CANCEL, title)
        confirm.format_secondary_text(content)
        if cb is not None:
            confirm.connect('response', cb)
        response = confirm.run()
        confirm.destroy()
        return response

    def on_btn_refresh_clicked(self, widget=None):
        """ 更新软件包 """
        url = f'https://www.atzlinux.com/allpackages-{self.arch}-v12-section.json'
        self.set_statusbar_text('正在下载软件包列表' + url)
        
        try:
            urlretrieve(url, allpackages_cache)
            
            # 构建分类列表
            section_model = Gtk.ListStore(str)
            section_model.append(['全部'])
            self.sections = ['全部']

            with open(allpackages_cache, 'r', encoding='utf-8') as f:
                allpackages = json.load(f)
                
            for data in allpackages:
                section = data.get('section', '全部')
                if section not in self.sections:
                    self.sections.append(section)
                    section_model.append([section])

            if hasattr(self, 'builder'):
                section_widget = self.builder.get_object('pkg_section')
                if section_widget:
                    section_widget.set_model(section_model)

            # 通过守护进程更新包列表
            if self.apt_client.connected:
                self.apt_client.execute_apt_command('apt update')
                self.set_statusbar_text('正在执行 apt update')
                
                # 获取已安装包列表
                self.apt_client.get_installed_packages()
            else:
                self.show_error_dialog("守护进程连接失败", "无法执行apt命令，请检查守护进程状态")
            
        except Exception as e:
            logging.error(f"刷新失败: {e}")
            self.set_statusbar_text(f'刷新失败: {e}')
    
    def on_confirm_install(self, dialog, response):
        if response != Gtk.ResponseType.OK:
            return
        pkgs = self.get_selected_packages()
        if self.apt_client.connected:
            command = 'apt install -y ' + ' '.join(pkgs)
            self.apt_client.execute_apt_command(command)
            self.set_statusbar_text('正在安装软件：' + ' '.join(pkgs))
        else:
            self.show_error_dialog("守护进程连接失败", "无法执行安装命令")
    
    def on_confirm_upgrade(self, dialog, response):
        if response != Gtk.ResponseType.OK:
            return
        if self.apt_client.connected:
            self.apt_client.execute_apt_command('apt upgrade -y')
            self.set_statusbar_text('正在执行系统软件升级')
        else:
            self.show_error_dialog("守护进程连接失败", "无法执行升级命令")
    
    def on_confirm_remove(self, dialog, response):
        if response != Gtk.ResponseType.OK:
            return
        pkgs = self.get_selected_packages()
        if self.apt_client.connected:
            command = 'apt remove --purge -y ' + ' '.join(pkgs)
            self.apt_client.execute_apt_command(command)
            self.set_statusbar_text('正在卸载软件：' + ' '.join(pkgs))
        else:
            self.show_error_dialog("守护进程连接失败", "无法执行卸载命令")

    def on_btn_install_clicked(self, widget):
        """ 安装软件包 """
        pkgs = self.get_selected_packages()
        cnt = len(pkgs)
        if cnt == 0:
            self.messagebox('提示', '选择您想安装的软件包')
            return
        if cnt > 20:
            self.messagebox('提示', '不建议一次安装超过20个软件包，您选择了%d个。' % cnt)
            return
            
        response = self.confirm_action('确认安装%d个软件包' % cnt, 
                                     '即将安装软件包清单如下，请确认：%s' % pkgs)
        if response == Gtk.ResponseType.OK:
            self.on_confirm_install(None, response)
    
    def on_btn_upgrade_clicked(self, widget):
        """ 一键升级软件包 """
        response = self.confirm_action('确认一键升级软件包', '即将自动升级系统中可升级的软件包，请确认')
        if response == Gtk.ResponseType.OK:
            self.on_confirm_upgrade(None, response)

    def on_btn_delete_clicked(self, widget):
        """ 卸载软件包 """
        pkgs = self.get_selected_packages()
        cnt = len(pkgs)
        if cnt == 0:
            self.messagebox('提示', '选择您想卸载的软件包')
            return
        if cnt > 20:
            self.messagebox('提示', '不建议一次卸载超过20个软件包，您选择了%d个。' % cnt)
            return
            
        response = self.confirm_action('确认卸载%d个软件包' % cnt,
                                     '即将卸载软件包清单如下，请确认：%s' % pkgs)
        if response == Gtk.ResponseType.OK:
            self.on_confirm_remove(None, response)

    def on_btn_view_more_clicked(self, widget):
        """ 查看详细日志 """
        logging.debug("点击查看详细日志")
        popover = self.builder.get_object('popover')
        popover.set_relative_to(widget)
        popover.show_all()
        popover.popup()

    def on_btn_select_all_clicked(self, widget):
        for row in self.model:
            row[0] = True

    def on_btn_deselect_all_clicked(self, widget):
        for row in self.model:
            row[0] = False
    
    def on_treeview_main_cursor_changed(self, widget):
        """ 选中表格行 """
        (row, col) = widget.get_cursor()
        if row is None:
            return

        pkg = widget.get_model()[row][1]
        if pkg in self.extra_infos:
            summary = self.extra_infos[pkg]['summary']
            summary = summary.replace('target="_blank"', '').replace('&', '&amp;')
            l = self.builder.get_object('label_pkg_summary')
            l.set_label(summary)
            l.set_use_markup(True)

    def on_treeview_main_row_activated(self, widget, row, col):
        """ 双击表格行时 """
        logging.info("用户双击第 %s 行。", row)

    def on_section_widget_changed(self, combo):
        tree_iter = combo.get_active_iter()
        if tree_iter is not None:
            model = combo.get_model()
            section = model[tree_iter][0]
            self.section = section
        else:
            self.section = '全部'
            combo.set_active(0)
        self.load_data_as_model()
        return True

    def on_cell_toggled(self, widget, path):
        self.model[path][0] = not self.model[path][0]

    def gtk_main_quit(self, widget):
        """ 用户点击关闭窗口 """
        logging.info("软件退出，正在清理连接……")
        if self.apt_client:
            self.apt_client.disconnect()
        Gtk.main_quit()

    def refresh_packages(self):
        """刷新软件包列表"""
        if self.apt_client.connected:
            self.apt_client.get_installed_packages()
        else:
            self.load_data_as_model()

    def set_statusbar_text(self, msg):
        """ 修改状态栏文字 """
        if not hasattr(self, 'builder'):
            return
            
        label = self.builder.get_object('label_statusbar')
        detail = self.builder.get_object('label_detail')
        
        if label:
            label.set_text(msg)
        if detail:
            orig = detail.get_text()
            orig = "\n".join(orig.split("\n")[-30:])
            detail.set_text(msg if orig == '' else orig + "\n" + msg)

    def ui_init(self):
        """ 初始化用户界面 """
        ui = os.path.realpath(os.path.dirname(__file__) + "/atzstore.glade")
        if not os.path.exists(ui):
            logging.error("无法打开glade文件，请检查路径：%s", ui)
            exit(1)
        logging.info("正在加载界面文件: %s", ui)
        
        self.builder.add_from_file(ui)
        win = self.builder.get_object('main_win')
        if win is None:
            logging.error("glade文件中无法找到窗口数据，%s", ui)
            exit(1)
        self.builder.connect_signals(self)
        self.win = win

        win.set_title(f'铜豌豆应用商店({self.arch}) - atzlinux store')
        top_label = self.builder.get_object('label_pkg_summary1')
        top_label.set_markup(f"<a href=\"https://www.atzlinux.com\">铜豌豆Linux</a> <a href=\"https://www.atzlinux.com/allpackages-{self.arch}.htm\">软件源网站</a> <a href=\"https://gitee.com/mikeliang/atzlinux-store/issues\">商店问题反馈</a> <a href=\"https://www.atzlinux.com/juanzeng.htm\">捐赠</a>")
        
        self.treeview = self.builder.get_object('treeview_main')
        search_entry = self.builder.get_object('search_entry')
        self.load_data_as_model()
        self.treeview.set_model(self.model)
        self.treeview.set_search_entry(search_entry)
        
        def search_func(model, column, key, row_iter):
            """search"""
            row = list(model[row_iter])
            k = key.lower()
            for i in [1, 2, 6]:
                if k in row[i].lower():
                    return False
            return True
            
        self.treeview.set_search_equal_func(search_func)
        
        for i, col in enumerate(self.columns):
            if i == 0:
                renderer = Gtk.CellRendererToggle()
                column = Gtk.TreeViewColumn(col[1], renderer, active=i)
                renderer.connect("toggled", self.on_cell_toggled)
            else:
                renderer = Gtk.CellRendererText()
                column = Gtk.TreeViewColumn(col[1], renderer, text=i)
            column.set_max_width(600)
            column.set_resizable(True)
            column.set_sort_column_id(i)
            self.treeview.append_column(column)
        
        section_widget = self.builder.get_object('pkg_section')
        renderer_text = Gtk.CellRendererText()
        section_widget.pack_start(renderer_text, True)
        section_widget.add_attribute(renderer_text, "text", 0)
        section_widget.set_active(0)
        section_widget.set_wrap_width(5)
        section_widget.connect("changed", self.on_section_widget_changed)

        win.maximize()
        win.show_all()

    def init(self):
        """初始化并启动UI"""
        self.ui_init()
        Gtk.main()

def store_main():
    """主函数"""
    logging.info("ATZStore UI started")
    
    def getpid():
        try:
            if not os.path.exists(pidfile):
                logging.info('No pidfile %s', pidfile)
                return False
            with open(pidfile) as fp:
                pid = int(fp.read())
                if pid <= 0:
                    logging.info('pid error %s', pid)
                    return False
            if not os.path.exists('/proc/%d' % pid):
                logging.info('No a valid pid %s', pid)
                return False
            with open('/proc/%d/cmdline' % pid, 'r') as fp:
                cmdline1 = fp.read().split('\x00')
            with open('/proc/%d/cmdline' % os.getpid(), 'r') as fp:
                cmdline2 = fp.read().split('\x00')
            if len(cmdline1) < 2 or len(cmdline2) < 2:
                return False
            if cmdline1[1] != cmdline2[1]:
                logging.info('cmd line not equal')
                return False
            logging.info('Current pid=%d', pid)
            return pid
        except (OSError, ValueError):
            logging.info('except')
            return False

    app = StoreWin()

    def sig_handler(signum, frame):
        """ 信号处理器 """
        app.win.present()

    pid = getpid()
    if pid == False:
        with open(pidfile, 'w') as fp:
            try:
                fp.write(str(os.getpid()))
            except (OSError, ValueError):
                logging.error('Unable to write pid file')

        # Run ui in the main thread
        signal.signal(signal.SIGUSR1, sig_handler)
        app.init()
    else:
        os.kill(pid, signal.SIGUSR1)
        logging.info('already running at pid %d, bringing to front', pid)

if __name__ == "__main__":
    store_main()